package org.jeecg.cn.edu.scau.vm.container.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.jeecg.cn.edu.scau.vm.common.consts.ContainerConst;
import org.jeecg.cn.edu.scau.vm.common.consts.DefaultConst;
import org.jeecg.cn.edu.scau.vm.common.utils.RedisIdWorker;
import org.jeecg.cn.edu.scau.vm.container.domain.VmContainer;
import org.jeecg.cn.edu.scau.vm.container.domain.VmContainerOperType;
import org.jeecg.cn.edu.scau.vm.container.domain.VmContainerUsage;
import org.jeecg.cn.edu.scau.vm.container.enums.ContainerOperationEnum;
import org.jeecg.cn.edu.scau.vm.container.enums.ContainerStatusEnum;
import org.jeecg.cn.edu.scau.vm.container.factory.UrlFactory;
import org.jeecg.cn.edu.scau.vm.container.mapper.VmContainerMapper;
import org.jeecg.cn.edu.scau.vm.container.request.CreateContainerRequest;
import org.jeecg.cn.edu.scau.vm.container.response.CreateContainerResponse;
import org.jeecg.cn.edu.scau.vm.container.service.VmContainerBizService;
import org.jeecg.cn.edu.scau.vm.container.service.VmContainerService;
import org.jeecg.cn.edu.scau.vm.container.service.VmContainerUsageService;
import org.jeecg.cn.edu.scau.vm.container.vo.VmContainerListVo;
import org.jeecg.cn.edu.scau.vm.container.vo.VmContainerVo;
import org.jeecg.cn.edu.scau.vm.image.domain.VmImage;
import org.jeecg.cn.edu.scau.vm.image.service.VmImageService;
import org.jeecg.cn.edu.scau.vm.node.domain.VmNode;
import org.jeecg.cn.edu.scau.vm.node.service.NodeService;
import org.jeecg.cn.edu.scau.vm.review.entity.VmSchedule;
import org.jeecg.cn.edu.scau.vm.review.enums.VmScheduleEnum;
import org.jeecg.cn.edu.scau.vm.review.service.impl.IVmScheduleService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.jeecg.cn.edu.scau.vm.common.consts.RedisConst.CONTAINER_TIME_PREFIX;
import static org.jeecg.cn.edu.scau.vm.common.consts.RedisConst.REMAINING_TIME_HASH_KEY;
import static org.jeecg.cn.edu.scau.vm.common.consts.ReturnConst.NO_ENOUGH_GPU_CARD;
import static org.jeecg.cn.edu.scau.vm.common.consts.ReturnConst.NO_ENOUGH_MACHINE_TIME;

@Service
public class VmContainerBizServiceImpl
        extends ServiceImpl<VmContainerMapper, VmContainer> implements VmContainerBizService {
    @Resource
    private VmContainerService vmContainerService;
    @Resource
    private VmImageService vmImageService;
    @Resource
    private VmContainerMapper vmContainerMapper;
    @Resource
    private NodeService nodeService;
    @Resource
    private VmContainerUsageService containerUsageService;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private ISysUserService userService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private IVmScheduleService scheduleService;
    @Resource
    private RedisIdWorker redisIdWorker;

    private static final Logger logger = LoggerFactory.getLogger(VmContainerBizServiceImpl.class);

    @Override
    public Page<VmContainerListVo> pageContainerDetail(Page<VmContainer> page, QueryWrapper<VmContainer> queryWrapper) {
        // 1. 先使用page查询相关数据
        logger.info("查询容器列表:{},{}", page, queryWrapper);
        Page<VmContainer> rawPage = this.page(page, queryWrapper);
        // 2. 渲染相关数据
        List<VmContainer> records = rawPage.getRecords();
        List<VmContainerListVo> collect = records.stream().map((vmContainer) -> {
            VmContainerListVo vmContainerListVo = new VmContainerListVo();
            // 2.1 复制属性
            BeanUtils.copyProperties(vmContainer, vmContainerListVo);
            // 2.2 查询对应的镜像数据
            VmImage vmImage = vmImageService.getByImageId(vmContainer.getImageId());
            vmContainerListVo.setVmImage(vmImage);
            // 2.3 查询对应的节点数据
            VmNode vmNode = nodeService.getNodeByNodeId(vmContainer.getNodeId());
            vmContainerListVo.setVmNode(vmNode);
            // 2.4 填充关于ssh端口的信息
            String sshPort = getSSHPort(vmContainerListVo.getPorts());
            vmContainerListVo.setSshPort(sshPort);
            vmContainerListVo.setPasswordBase64(Base64.getEncoder().encodeToString(vmContainerListVo.getSshPassword().getBytes(StandardCharsets.UTF_8)));
            // 2.5 填充关于容器状态的信息
            Integer statusCode = vmContainerListVo.getStatus();
            ContainerStatusEnum enumByCode = ContainerConst.getEnumByCode(statusCode);
            vmContainerListVo.setContainerStatusName(enumByCode.getMessage());
            vmContainerListVo.setContainerStatusType(enumByCode.getStatusType());
            // 2.6 添加容器拥有者的名字,获取用户名
            SysUser createUser = userService.getById(vmContainer.getCreateBy());
            vmContainerListVo.setOwner(createUser.getUsername());
            // 2.7 获取剩余机时信息
            String key = CONTAINER_TIME_PREFIX + vmContainer.getContainerId();
            Long expire = stringRedisTemplate.getExpire(key, TimeUnit.MINUTES);
            if (Objects.isNull(expire) || expire == -2L) {
                expire = 0L;
            } else {
                expire++;
            }
            vmContainerListVo.setLeaveMachineTime(expire);
            return vmContainerListVo;
        }).collect(Collectors.toList());
        Page<VmContainerListVo> targetPage = new Page<>();
        BeanUtils.copyProperties(rawPage, targetPage, "records");
        targetPage.setRecords(collect);
        return targetPage;
    }

    private String getSSHPort(String ports) {
        if (Objects.isNull(ports) || ports.isEmpty()) {
            return "";
        }
        // 多个端口采用分割
        String[] split = ports.split(";");
        for (String s : split) {
            if (s.startsWith("22/tcp")) {
                // 证明这个是代表端口映射的端口,然后从后向前
                return s.substring(s.lastIndexOf(':') + 1);
            }
        }
        return "";
    }


    @Override
    @Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
    public Result<String> createContainer(VmContainerVo vmContainerVo) {
        // 1. 校验镜像信息
        VmImage vmImage = vmImageService.getByImageId(vmContainerVo.getImageId());
        if (Objects.isNull(vmImage)) {
            return Result.error("无法查找到此镜像");
        }
        if (Objects.isNull(vmImage.getCreateTimeOut()) ||
                Objects.isNull(vmImage.getCommand())) {
            return Result.error("镜像信息不完整!");
        }
        // 2. 选择合适的节点
        VmNode vmNode = nodeService.selectNode();
        if (Objects.isNull(vmNode)) {
            return Result.error("当前没有合适的节点!");
        }
        // 3 判断当前用户是否的机时是否足够
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        if (Objects.isNull(loginUser)) {
            return Result.error("用户未登录!");
        }
        // 3.1 判断当前用户的gpu数量是否足够?
        VmContainerUsage usage = containerUsageService.getByUserId(loginUser.getId());
        if (usage.getRemainIngTime() < vmContainerVo.getMachineTime()) {
            return Result.error(NO_ENOUGH_MACHINE_TIME, "您当月可用的机时不足,是否创建审核?");
        }
        if (usage.getRemainGpuCardNumber() < vmContainerVo.getGpuCardNumber()) {
            return Result.error(NO_ENOUGH_GPU_CARD, "您当月可用的GPU卡数不足,是否创建审核?");
        }
        // 3.2 预扣减
        if (!containerUsageService.tryAllocateMachineTime(loginUser, vmContainerVo.getMachineTime())) {
            return Result.error(NO_ENOUGH_MACHINE_TIME, "您当月可用的机时不足,是否创建审核?");
        }
        return createContainer(vmImage, loginUser, vmContainerVo, vmNode);
    }

    @Override
    public Result<String> createContainer(VmImage vmImage, LoginUser loginUser, VmContainerVo vmContainerVo, VmNode vmNode) {
        // 4. 填充基础业务数据
        vmContainerVo.setCreateBy(loginUser.getId());
        vmContainerVo.setUpdateBy(loginUser.getId());
        vmContainerVo.setSysOrgCode(loginUser.getOrgCode());
        vmContainerVo.setCreateTime(LocalDateTime.now());
        vmContainerVo.setUpdateTime(LocalDateTime.now());
        vmContainerVo.setNodeId(vmNode.getNodeId());
        // 5. 插入数据
        vmContainerMapper.insert(vmContainerVo);
        logger.info("创建容器的基本信息为:{}", vmContainerVo);
        // 6. 发布消息 || 执行HTTP调用 || 弹出进度条形式
        if (vmContainerVo.isFastCreate()) {
            return vmContainerService.containerCreateEventPublish(vmContainerVo, vmImage.getCommand(), String.valueOf(vmImage.getCreateTimeOut()));
        }
        if (vmImage.getCreateTimeOut() <= DefaultConst.DEFAULT_FOR_SKIP_WAITING) {
            // 时间可以忍受,执行HTTP调用
            CreateContainerRequest request = CreateContainerRequest.builder()
                    .command(vmImage.getCommand().replaceAll("\\r?\\n$", ""))
                    .timeout(String.valueOf(TimeUnit.SECONDS.toMillis(vmImage.getCreateTimeOut())))
                    .tableId(vmContainerVo.getId())
                    .build();
            // 执行HTTP调用
            // 6.1 如果创建成功
            String url = UrlFactory.getUrl(ContainerConst.CREATE, vmNode.getNodeIpAddress());
            logger.info("远程调用的url为:{}", url);

            CreateContainerResponse createContainerResponse = restTemplate.postForObject(url, request, CreateContainerResponse.class);
            if (this.handleCreateContainerSuccess(createContainerResponse, vmImage)) {
                return Result.ok("创建容器成功!");
            }
            // 6.2 如果创建失败
            VmContainer vmContainer = VmContainer.builder()
                    .id(vmContainerVo.getId())
                    .status(ContainerStatusEnum.CREATE_FAILED.getCode())
                    .updateTime(LocalDateTime.now())
                    .build();
            this.updateById(vmContainer);
            logger.info("创建容器失败!容器状态更新完毕,{}", vmContainer);
            return Result.error("创建容器失败!");
        }
        return vmContainerService.containerCreateEventPublish(vmContainerVo, vmImage.getCommand(), String.valueOf(TimeUnit.SECONDS.toMillis(vmImage.getCreateTimeOut())));
    }


    @Override
    public Result<String> stopContainer(String containerId) {
        // 1. 修改数据状态为停用中
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        return this.stopContainer(containerId, loginUser);
    }

    @Override
    public void removeContainerByContainerId(String containerId) {
        QueryWrapper<VmContainer> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("container_id", containerId);
        this.remove(queryWrapper);
    }

    @Override
    public Result<String> stopContainer(String containerId, LoginUser loginUser) {
        vmContainerMapper.updateStatusById(ContainerStatusEnum.STOPPING.getCode(), containerId, LocalDateTime.now(), loginUser.getId());
        // 2. 查询容器对应的节点
        VmContainer vmContainer = this.getByContainerId(containerId);
        // 3. 查询容器剩余机时
        String key = CONTAINER_TIME_PREFIX + containerId;
        // 如果key已经不存在了,那么就发布停止的消息
        logger.info("查询出来的vmContainer为{}", vmContainer);
        if (Boolean.FALSE.equals(stringRedisTemplate.hasKey(key))) {
            return vmContainerService.containerStopEventPublish(vmContainer.getNodeId(), containerId, vmContainer.getId());
        }
        String rawValue = (String) stringRedisTemplate.opsForHash().get(key, REMAINING_TIME_HASH_KEY);
        if (Objects.isNull(rawValue)) {
            return Result.error("机时计算产生异常!");
        }
        // 3.1 覆写当前过期时间
        stringRedisTemplate.opsForHash().put(key, REMAINING_TIME_HASH_KEY, rawValue);
        // 3.2 设置为永不过期
        stringRedisTemplate.persist(key);
        // 4. 查询到节点后,执行发布
        return vmContainerService.containerStopEventPublish(vmContainer.getNodeId(), containerId, vmContainer.getId());
    }

    @Override
    public Result<String> removeContainer(String containerId) {
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        // 1. 机时计算与机时返还设计
        // 1.1 删除只会是停用容器的状态,因此先读取值
        String key = CONTAINER_TIME_PREFIX + containerId;
        String remainingTime = (String) stringRedisTemplate.opsForHash().get(key, REMAINING_TIME_HASH_KEY);
        // 3.2 删除本key
        stringRedisTemplate.delete(key);
        if (!Objects.isNull(remainingTime)) {
            // 证明这个容器还没过期,需要返回一部分机时
            try {
                containerUsageService.addRemainIngTime(loginUser.getId(), Integer.valueOf(remainingTime), 5);
            } catch (Exception e) {
                logger.error("更新机时间产生异常!{}", e.getMessage(), e);
            }
        }
        // 3.3 提交到数据库
        // 2. 修改数据状态为删除中
        vmContainerMapper.updateStatusById(ContainerStatusEnum.DELETING.getCode(), containerId, LocalDateTime.now(), loginUser.getId());
        // 3. 查询容器对应的节点
        logger.info("删除容器:{}", containerId);
        QueryWrapper<VmContainer> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("container_id", containerId);
        VmContainer vmContainer = vmContainerMapper.selectOne(queryWrapper);
        // 4. 查询到节点后,执行发布
        return vmContainerService.containerRemoveEventPublish(vmContainer.getNodeId(), containerId, vmContainer.getId());
    }


    @Override
    public Result<String> startContainer(String containerId) {
        // 1. 修改数据状态为启动中
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        vmContainerMapper.updateStatusById(ContainerStatusEnum.STARTING.getCode(), containerId, LocalDateTime.now(), loginUser.getId());
        // 2. 查询容器对应的节点
        VmContainer vmContainer = this.getByContainerId(containerId);
        // 3. 恢复容器机时
        // 3.1 读取键中的过期时间
        String key = CONTAINER_TIME_PREFIX + containerId;
        String remainingTime = (String) stringRedisTemplate.opsForHash().get(key, REMAINING_TIME_HASH_KEY);
        logger.info("获得的剩余机时为{}", remainingTime);
        if (Objects.isNull(remainingTime)) {
            return Result.error("机时计算产生异常!");
        }
        // 3.2 恢复过期时间
        stringRedisTemplate.expire(key, Integer.parseInt(remainingTime), TimeUnit.SECONDS);
        // 4. 查询到节点后,执行发布
        return vmContainerService.containerStartEventPublish(vmContainer.getNodeId(), containerId, vmContainer.getId());
    }

    @Override
    public boolean updateStatus(String id, Integer status, LocalDateTime updateTime) {
        VmContainer vmContainer = VmContainer.builder()
                .id(id)
                .status(status)
                .updateTime(updateTime)
                .build();
        return this.updateById(vmContainer);
    }

    @Override
    public void handleContainerExpire(String expireKey) {
        // 1. 判断是否是我们想要的key
        logger.info("监听到key过期,名称为:{}", expireKey);
        if (Objects.isNull(expireKey) || expireKey.isEmpty()) {
            return;
        }
        // 2. 含有这个标志,那么就进入处理阶段
        if (!expireKey.startsWith(CONTAINER_TIME_PREFIX)) {
            return;
        }
        // 3. 获取容器ID
        String containerId = expireKey.substring(CONTAINER_TIME_PREFIX.length());
        logger.info("容器{}过期了", containerId);
        // 随即停用容器
        LoginUser loginUser = new LoginUser();
        loginUser.setId("e9ca23d68d884d4ebb19d07889727dae");
        this.stopContainer(containerId, loginUser);
    }

    @Override
    public Result<Map<Integer, String>> getContainerStatusEnum() {
        ContainerStatusEnum[] values = ContainerStatusEnum.values();
        Map<Integer, String> map = new HashMap<>(values.length);
        for (ContainerStatusEnum value : values) {
            map.put(value.getCode(), value.getMessage());
        }
        return Result.ok(map);
    }


    @Override
    public Boolean handleCreateContainerSuccess(CreateContainerResponse response, VmImage vmImage) {
        if (Objects.isNull(response) || "".equals(response.getContainerId())) {
            logger.error("创建容器失败!无容器ID,日志信息:{}", response.getMessage());
            return Boolean.FALSE;
        }
        // 查询对应的镜像,获取账密
        VmContainer rawType = this.getById(response.getTableId());
        if (Objects.isNull(vmImage)) {
            vmImage = vmImageService.getByImageId(rawType.getImageId());
        }
        // 2. 否则的话执行更新,需要查询镜像的相关信息,填入默认账号密码
        VmContainer vmContainer = VmContainer.builder()
                .containerId(response.getContainerId())
                .ports(response.getPortMappingInfo())
                .sshUserName(vmImage.getSshUsername())
                .sshPassword(vmImage.getSshPassword())
                .id(response.getTableId())
                .nodeId(response.getNodeId())
                .status(ContainerStatusEnum.RUNNING.getCode())
                .updateTime(LocalDateTime.now())
                .build();
        this.updateById(vmContainer);
        // 创建键值对
        String key = CONTAINER_TIME_PREFIX + vmContainer.getContainerId();
        // 记录秒数
        stringRedisTemplate.opsForHash().put(key, REMAINING_TIME_HASH_KEY, String.valueOf(TimeUnit.MINUTES.toSeconds(rawType.getMachineTime())));
        // 设置过期时间
        stringRedisTemplate.expire(key, rawType.getMachineTime(), TimeUnit.MINUTES);
        // TODO:添加过期监控任务
        logger.info("创建容器{}成功!容器状态更新完毕,{}", vmContainer.getContainerId(), vmContainer);
        return Boolean.TRUE;
    }


    @Override
    public List<VmContainerOperType> getOperationType(String id) {
        VmContainer vmContainer = this.getById(id);
        // 1. 获取容器当前状态
        if (Objects.isNull(vmContainer)) {
            return Collections.emptyList();
        }
        int status = vmContainer.getStatus();
        // 2. 根据状态计算当前可用的类型
        // 2.1 正在运行中,只允许直接停止
        List<VmContainerOperType> result = new ArrayList<>();
        if (ContainerStatusEnum.RUNNING.getCode() == status) {
            result.add(new VmContainerOperType(ContainerOperationEnum.STOP_CONTAINER));
            // 2.2 已停止,可以启动,或者直接删除
        } else if (ContainerStatusEnum.STOPPED.getCode() == status) {
            result.add(new VmContainerOperType(ContainerOperationEnum.DELETE_CONTAINER));
            result.add(new VmContainerOperType(ContainerOperationEnum.START_CONTAINER));
        }
        return result;
    }

    @Override
    public VmContainer getByContainerId(String containerId) {
        QueryWrapper<VmContainer> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("container_id", containerId);
        return vmContainerMapper.selectOne(queryWrapper);
    }

    @Override
    public List<VmContainer> getByNodeId(String nodeId) {
        QueryWrapper<VmContainer> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("node_id", nodeId);
        return vmContainerMapper.selectList(queryWrapper);
    }


    @Override
    public Result<String> createSpecialSchedule(VmContainerVo vmContainerVo) {
        // 1. 装填相关数据
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        VmSchedule vmSchedule = VmSchedule.builder()
                .scheduleUuid(redisIdWorker.nextId("vm_schedule"))
                .createBy(loginUser.getId())
                .updateBy(loginUser.getId())
                .gpuNum(vmContainerVo.getGpuCardNumber())
                .machineTime(vmContainerVo.getMachineTime())
                .approveStatus(VmScheduleEnum.PROCESSING.getCode())
                .description(vmContainerVo.getDescription())
                .name(vmContainerVo.getName())
                .reason(vmContainerVo.getReason())
                .sysOrgCode(loginUser.getOrgCode())
                .userId(loginUser.getId())
                .imageId(vmContainerVo.getImageId())
                .userName(loginUser.getUsername())
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .build();
        scheduleService.save(vmSchedule);
        return Result.ok("创建成功!");
    }
}
